iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
Software Development

用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道系列 第 25 篇

Day 25:生成式 UI - 在 PySide6 中由資料動態產生 UI 元件

  • 分享至 

  • xImage
  •  

目的:讀者可以讓 GUI 程式在使用過程中生成 UI 元件,並瞭解如何設計一個單一窗口管理與讀取這些額外生成之 UI 元件

前言

我們在 day 13 中知道可以使用 QScrollArea 等容器來放置大量放置 UI 物件,且有提到 QScrollArea 可自動產生卷軸,因此適合放置 GUI 程式執行期間會額外生成的 UI 元件。

在今天的文章中,我們從抓取檔案路徑開始,實作一個可以依照選取資料數量、自動產生QCheckBox 的單一窗口。

動態產生 UI 元件的應用情景

在 day 9 中,我們使用 QT designer 的目的就是要用畫面去建構人看的畫面、而不要用文字去建構。但在以下情況還是需要用 code 在執行期間自動產生元件:

  • UI 元件的內容與 GUI 程式執行時產生的資料高度相關。例如選擇檔案後產生 QCheckBox 讓 user 選擇那些 wafer 需要分析、依照不同產品別生成不同 SEPC 輸入框讓使用者容易輸入

  • 重複性高、高度結構化的內容,例如多組量測項目的最大/ 小值輸入框。我們可以利用自動生成元件減少製作 UI 時的工作量 (就是少拉幾次元件啦)

動態生成的元件的特點在於它們沒有一個像 button_start 這樣明確的名稱,但它們是真實的存在於記憶體與畫面中。因此在今天的文章中我們會設計單一窗口 (就是 class) 來統一管理這些執行時生成的額外 UI 元件。

如何讓使用者選擇檔案

我們都很習慣在使用 office 軟體時,從按下”開啟舊檔”後彈出的視窗中選擇檔案。而在 QT 環境中我們可以呼叫 QFileDialog.getOpenFileNames(……) 讓 user 可以在相同的視窗中選擇檔案。getOpenFileNames 是多選,選擇後會回傳檔案路徑清單與 filter (不重要)。

檔案路徑清單的型別是 list[str],若需要轉成 list[Path] 需自行處理。

getOpenFileNames 的個別引數意義在此

file_names, _ = QFileDialog.getOpenFileNames(
    parent=self.ui_window.centralwidget,
    caption="選取檢測檔案",
    dir="",
    filter="CSV 檔案 (*.csv);;所有檔案 (*)",
)

除了多選檔案以外, QT 還提供多種檔案選擇方式:

應用例:動態生成 UI 元件的管理器

我們今天的目的就是藉由程式執行期間生成的資料(也就是上面產生的檔案路徑清單),產生動態元件:寫著 wafer ID 的 QCheckBox。這些物件可以讓 GUI 程式計算 user 選擇的檔案,不用重複選擇檔案讓程式更加靈活。

今天會示範做出以下功能之 GUI 程式:使用者讀檔後生成 ,並且 user 按下執行時,程式可以依照 function 與使用者選擇做出對應行為。在本案例中的呈現方式是顯示選擇檔案的路徑。

https://i.meee.com.tw/xmHzJfE.gif

管理器的初始化

這個管理器負責生成 UI 元件並放入指定容器中,因此管理器需要記得以下 3 個資訊,其中容器與佈局是從外面傳進來。

  • 容器 (container):生成的元件需要有一個容器來管理它,這裡會是傳入 QScrollArea 的子 QWidget

  • 布局 (layout):指定生成物件擺放是直的還是橫的,可以在 QT designer 中預先設定

  • 物件清單:存放在這個 class 中生成的物件,在這裡我們使用 tuple[QCheckBox, str] 儲存是方便讀取 QCheckBox 的狀態後,輸出旁邊的檔案名稱

class DynamicCheckboxManager:
    def __init__(self, layout: QLayout, container: QWidget) -> None:
        self._layout = layout
        self._container = container
        self._items: list[tuple[QCheckBox, str]] = []

生成元件並納入管理

我們設定的使用方式是先建立這個管理器,然後呼叫生成元件 api 建立動態元件。這個 api 的流程如下:

移除舊元件 → 建立新元件 → 新元件放入 layout → 新元件放入物件清單

其中在移除舊元件時,我們需要將舊元件從 layout 以及記憶體中移除。從 layout 中移除可以使用 layout.removeWidget(物件),這個移除只是讓 user 看不到而已、物件還存在在記憶體之中。要將物件乾淨移除,我們還需要呼叫 物件.deleteLater() 將物件從記憶體刪除。

關於 deleteLater() 的意義,在 day 26 會有更詳盡的說明

    def set_checkbox(self, parts: Iterable[object]) -> None:
        """根據傳入之資料設定 check box 於指定 layout 內"""
        self.clear()

        for part in parts:
            _part = str(part)
            checkbox = QCheckBox(_part, self._container)
            self._layout.addWidget(checkbox)
            self._items.append((checkbox, _part))

    def clear(self) -> None:
        """把 layout 內的 check box 全部清空"""

        if len(self._items) == 0:
            return

        for cb, _ in self._items:
            self._layout.removeWidget(cb)
            cb.deleteLater()

        self._items.clear()

取值介面的設計

取值時我們也只需要呼叫取值 api 即可。取值 api 會設計成只回傳乾淨的使用者資料,不要回傳過程中生成的物件資訊。Controller 不需要知道這些物件,只需要知道使用者選擇 /輸入甚麼並串接到下一個對應的物件。

以本案例來說,只會回傳被勾選的項目名稱清單。

    def get_checked_parts(self) -> list[str]:
        return [
            part
            for checkbox, part in self._items
            if checkbox.isChecked()
        ]

元件的管理器與 controller 的串接

這個管理器的使用方式就跟 day 19 的管理器一樣,我們先在 controller 中將其實例化,另外管理器前記得加上 self.。

class MainController:
    def __init__(self, ui_window: Ui_MainWindow) -> None:

        self.ui_window = ui_window
        # 存放片號與對應路徑之dict,方便接收使用者選擇後取出對應 path
        self.path_dict: dict[str, Path] = {}  

        # 建立管理器
        self.checkbox_obj = DynamicCheckboxManager(
            self.ui_window.verticalLayout_sa, self.ui_window.sa_contents)

我們的示範例中有 2 個按鍵,其中一個是選擇檔案。我們可以將 getOpenFileNames 輸出的路徑字串清單轉換成等一下方便檢索的 dict[str, Path] 格式。並且在這 function 中我們也薑片號清單傳入生成元件 api 建立動態物件。

def get_file_list_and_set_checkbox(self):
    """選擇檔案後,將檔案名稱之 checkbox 加到 UI 中"""
      file_names, _ = QFileDialog.getOpenFileNames(
          parent=self.ui_window.centralwidget,
          caption="選取檢測檔案",
          dir="",
          filter="CSV 檔案 (*.csv);;所有檔案 (*)",
      )

    # 取消選檔時保留目前畫面與清單
    if not file_names:
        return

    # 將選擇結果寫入 property 內
    _path_list = [
        [Path(path_str).stem, Path(path_str)]
        for path_str in file_names
    ]
    self.path_dict = dict(_path_list)

    # 加入 checkbox
    item_list = list(self.path_dict.keys())
    self.checkbox_obj.set_checkbox(item_list)

接下來是接收使用者選擇並做出對應行為。在這個示範例中我們是顯示使用者選擇的對應路徑,在專案實作時我們可以將檔名清單當篩選條件等多種應用。

def show_check_file_path(self):
    """把勾選的檔案對應路徑顯示於 UI 中"""
    check_list = self.checkbox_obj.get_checked_parts()
    msg_lines = []

    self.ui_window.te_path_viewer.clear()
    if not check_list:
        return

    for part in check_list:
        path = self.path_dict[part]
        msg_lines.append(f"Wafer {part} 的路徑: {path}")

    self.ui_window.te_path_viewer.setText("\n".join(msg_lines))

剛剛設定的 function 記得與按鈕連接。

def make_connection(self):
    self.ui_window.bn_get_path_list.clicked.connect(
        self.get_file_list_and_set_checkbox
    )
    self.ui_window.bn_show_path.clicked.connect(
        self.show_check_file_path
    )

To be continued →

我們今天知道了如何在 GUI 程式執行時自動生成元件以及管理它們。

在明天 day 26 的文章中,我們要來處理一個使用者常遇到的問題:畫面卡住不動。我們會利用 QThread 來處理這種跑大型迴圈時,畫面就卡住不動的問題。


上一篇
Day 24:把防呆寫在基因裡 - 使用 Pydantic 做進階資料意義的驗證
系列文
用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言